package by.vr.jpa.result.transformer;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.persistence.Id;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.hql.antlr.HqlTokenTypes;
import org.hibernate.hql.ast.HqlParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import antlr.RecognitionException;
import antlr.TokenStreamException;
import antlr.collections.AST;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;




public class ArrayToEntityResultTransformer  {
	
	public static final String DOT = ".";
	
	public static final char DOT_CHAR = '.';
	
	public static final String DOT_REGEX = "\\.";
	
	public static final String ID_KEYWORD = "id";
	
	public static final Class ID_ANNOTATION = Id.class;
	
	
	
	private static final Logger logger = LoggerFactory.getLogger(ArrayToEntityResultTransformer.class);
	
	
	
	private Class entityClass;
	
	private QueryInfo queryInfo;
	
	private Map<Class, String> idPropertyMap = Maps.newHashMap();
	
	
	

	

	

	public List transformList(List collection) {
		if(logger.isDebugEnabled()){
			logger.debug("transformList (collection = [" + collection + "]");
		}
		
		/*
		final List resultList = Lists.newArrayList();
		final List<String> properties = queryInfo.getProperties();
		
		if(CollectionUtils.isNotEmpty(collection)){
			
			Object[] dataArray;
			int dataLength ;
			Object value;
			String property;
			int propertyLength = properties.size();
			String propertyParts[];
			int propertyPartCount;
			Object currentEntity;
			String idProperty;
			Map<EntityKey, Object> entityMap = Maps.newHashMap();
			Object entity;
			String entityPath;
			String entityProperty;
			String currentEntityPath = null;
			Class propertyType;
			boolean isCollection;
			String entityParentPath;
			Object entityParent;
			Map<String, Object> rowEntityMap;
			String entityParentProperty;
			Collection entityCollection;
			try{
			for(Object dataObj : collection){
				dataArray = (Object[]) dataObj;
				dataLength = dataArray.length;
				if(propertyLength != dataLength){
					throw new IllegalArgumentException("Invalid properties size: dataLength=" + dataLength + ", propertiesLength=" + propertyLength);
				}
				currentEntity = null;
				currentEntityPath = null;
				rowEntityMap = Maps.newHashMap();
				for(int i = 0; i < dataLength; i++){
					value = dataArray[i];
					property = properties.get(i);
					
					propertyParts = property.split(DOT_REGEX);
					propertyPartCount = propertyParts.length;
					
					if(propertyPartCount > 1){
						
						entityPath = getEntityPath(propertyParts);
						entityProperty = getEntityProperty(propertyParts);
						entityParentPath = getEntityParentPath(propertyParts);
						entityParentProperty = getEntityParentProperty(propertyParts);
						
						if(ObjectUtils.equals(currentEntityPath, entityPath)){
							PropertyUtils.setProperty(currentEntity, entityProperty, value);
							} else {
								entityParent = getEntityParent(rowEntityMap, entityParentPath);
								propertyType = PropertyUtils.getPropertyType(entityParent, entityParentProperty);
								isCollection = isCollection(propertyType);

								idProperty = getIdProperty(propertyType, entityProperty);
								entity = getFromEntityMap(entityMap, idProperty, value);
								if (entity != null) {
									currentEntity = entity;
								} else {
									entity = createInstance(propertyType);
									PropertyUtils.setProperty(entity, idProperty, value);
									if (isCollection) {
										entityCollection = createCollection(propertyType);
										PropertyUtils.setProperty(entityParent, entityParentProperty, entityCollection);
										entityCollection.add(entity);

									} else {
										PropertyUtils.setProperty(entityParent, entityParentProperty, entity);
									}
									putToEntityMap(entityMap, idProperty, value, entity);
									rowEntityMap.put(entityPath, entity);
									currentEntity = entity;
									currentEntityPath = entityPath;
								}

							}
						
						
						
					}else{
						if(currentEntity != null){
							PropertyUtils.setProperty(currentEntity, property, value);
						}else{
							idProperty = getIdProperty(entityClass, property);
							entity = getFromEntityMap(entityMap, idProperty, value);
							if(entity != null){
								currentEntity = entity;
							}else{
								entity = createInstance(entityClass);
								PropertyUtils.setProperty(entity, idProperty, value);
								resultList.add(entity);
								putToEntityMap(entityMap, idProperty, value, entity);
								rowEntityMap.put(DOT, entity);
								currentEntity = entity;
							}
							
						}
					}
					
					
				}
				
				
				
				
			}
			}catch(Exception e){
				logger.error("", e);
				throw new RuntimeException(e);
			}
			
			
		}
		
		if(logger.isDebugEnabled()){
			logger.debug("Result list: " + resultList);
		}*/
		
		return null;
	}
	
	protected Object getEntityParent(final Map<String, Object> rowEntityMap, final String entityPath){
		final String key = StringUtils.isNotBlank(entityPath) ? entityPath : DOT;
		return rowEntityMap.get(key);
	}
	
	
	protected boolean isCollection(Class propertyType){
		return Collection.class.isAssignableFrom(propertyType);
	}
	
	
	protected Collection createCollection(Class propertyType){
		if(List.class.isAssignableFrom(propertyType)){
			return Lists.newArrayList();
		}else if(Set.class.isAssignableFrom(propertyType)){
			return Sets.newHashSet();
		}else{
			throw new IllegalArgumentException("Unsuported collection : " + propertyType);
		}
	}
	
	
	protected String getEntityParentPath(final String propertyParts[]){
		return excludeLast(propertyParts, 2);
	}
	
	
	protected String getEntityPath(final String propertyParts[]){
		return excludeLast(propertyParts, 1);
	}
	
	protected String excludeLast(final String propertyParts[], final int count){
		final int length = propertyParts.length;
		final StringBuilder sb = new StringBuilder(length);
		for(int i = 0; i < length-count;i++){
			sb.append(propertyParts[i]).append(DOT);
		}
		removeLastDot(sb);
		return sb.toString();
	}
	
	
	protected String getEntityProperty(final String propertyParts[]){
		final int length = propertyParts.length;
		if(length > 0){
			return propertyParts[length -1];
		}else{
			return  null;
		}
	}
	
	protected String getEntityParentProperty(final String propertyParts[]){
		final int length = propertyParts.length;
		if(length > 1){
			return propertyParts[length -2];
		}else{
			return  null;
		}
	}
	
	
	protected Object createInstance(final Class entityClass) throws Exception{
		
		return entityClass.newInstance();
	}
	
	
	protected String getIdProperty(final Class entityClass, final String property){
		final String idProperty = getPropertyIdAnnotated(entityClass);
		if(StringUtils.isBlank(idProperty)){
			throw new IllegalArgumentException("Expected id in the class: " + entityClass );
		}
		if(ID_KEYWORD.equals(property)){
			
			
			
			
			return idProperty;
		}else{
			
			if(ObjectUtils.notEqual(idProperty, property)){
				throw new IllegalArgumentException("Expected property=" + idProperty + ", but actual=" + property );
			}
			
			return property;
		}
	}
	
	
	protected String getPropertyIdAnnotated(final Class entityClass){
		String property = idPropertyMap.get(entityClass);
		if(property == null){
			final Field fields[] = entityClass.getDeclaredFields();
			for(Field f : fields){
				if(f.getAnnotation(ID_ANNOTATION) != null){
					property =  f.getName();
					break;
				}
			}
		
		
			if(property == null){
				String methodName = null;
				final Method methods[] = entityClass.getDeclaredMethods();
				for(Method m : methods){
					if(m.getAnnotation(ID_ANNOTATION) != null){
						methodName = m.getName();
						break;
					}
				}
				property = getPropertyFromMethodName(methodName);
			
			}
		}
		idPropertyMap.put(entityClass, property);
		return property;
	}
	
	
	protected String getPropertyFromMethodName(final String methodName){
		if(StringUtils.isNotBlank(methodName)){
			final StringBuilder sb = new StringBuilder(methodName);
			sb.delete(0, 2);
			final char firstChar = sb.charAt(0);
			sb.setCharAt(0, Character.toLowerCase(firstChar));
			return sb.toString();
		}else{
			return null;
		}
	}
	
	
	protected Object getFromEntityMap(final Map<EntityKey, Object> entityMap, final String property, final Object value){
		return entityMap.get(getKey(property, value));
	}
	
	
	protected void putToEntityMap(final Map<EntityKey, Object> entityMap, final String property, final Object value, final Object entity){
		entityMap.put(getKey(property, value), entity);
	}
	
	
	protected EntityKey getKey(final String property, final Object value){
		return new EntityKey(property, value);
	}
	
	
	protected void prepareQueryInfo(final QueryInfo queryInfo ){
		final Map<String, String> aliasMap = queryInfo.getJoinAliasMap();
	
		if(!aliasMap.isEmpty()){
    		String firstProperty;
    		String propertyParts[];
    		String aliasValue;
    		String alias;
    		String property;
    		boolean changed = true;
			while (changed) {
				changed = false;

				for (Entry<String, String> entry : aliasMap.entrySet()) {
					alias = entry.getKey();
					property = entry.getValue();
					propertyParts = property.split("\\.");
					if (propertyParts.length > 0) {
						firstProperty = propertyParts[0];
						if (aliasMap.containsKey(firstProperty)) {
							aliasValue = aliasMap.get(firstProperty);
							property = property.replaceFirst(firstProperty,
									aliasValue);
							aliasMap.put(alias, property);
							changed = true;
						}
					}

				}
    		}
		}
	}
	
	
	
	protected QueryInfo getQueryInfo(final String hql) throws RecognitionException, TokenStreamException{
		HqlParser parser = HqlParser.getInstance(hql );
		parser.statement();
		AST  hqlAst =  parser.getAST();
		
		final QueryInfo queryInfo = parseAst(hqlAst);
		prepareQueryInfo(queryInfo);
		preparePropertyNames(queryInfo);
		
		
		
		if(logger.isDebugEnabled()){
			logger.debug("Query info: " + queryInfo);
		}
		//TODO add cache
		return queryInfo;
	}
	
	
	
	protected void preparePropertyNames(final QueryInfo queryInfo){
		
		final List<String> projections = queryInfo.getProjections();
		final Map<String, String> aliasMap = queryInfo.getJoinAliasMap();
		final List<String> properties = Lists.newArrayList();
		final String entityClassName = queryInfo.getEntityName() + DOT;
		
		if(CollectionUtils.isNotEmpty(projections)){
			String propertyParts[];
			String alias;
			String aliasValue;
			for(String projection : projections){
				propertyParts = projection.split(DOT_REGEX);
				if(propertyParts.length > 0){
					alias = propertyParts[0];
					aliasValue = aliasMap.get(alias);
					if(aliasValue != null){
						projection = projection.replaceFirst(alias, aliasValue);
					}
				}
				
				projection = projection.replaceAll(entityClassName, "");
				
				
				properties.add(projection);
			}
		}
		queryInfo.setProperties(properties);
		
	}
	
	
	
	
	protected QueryInfo parseAst(AST hqlAst){
		if(logger.isDebugEnabled()){
			logger.debug("HQL ast : " + hqlAst.toStringTree());
		}
		
	
		
		
		if( isType(hqlAst,  HqlTokenTypes.QUERY)){
			
			final QueryInfo queryInfo = new QueryInfo();
			
			final AST selectFrom = hqlAst.getFirstChild();
			if(isType(selectFrom, HqlTokenTypes.SELECT_FROM) ){
				final AST from = selectFrom.getFirstChild();
				parseFrom(from, queryInfo);
				final AST select = from.getNextSibling();
				parseSelect(select, queryInfo);
			}
			
			
			
			return queryInfo;
			
		}else{
			throw new IllegalArgumentException("Hql is not a query");
		}
		
	}
	
	
	protected void parseSelect(AST select, QueryInfo queryInfo){
		if(isType(select, HqlTokenTypes.SELECT)){
			AST selectNode = select;
			while(selectNode.getNumberOfChildren() != 0){
				selectNode = selectNode.getFirstChild();
			}
			final List<String> projections = Lists.newArrayList();
			String projection;
			while((selectNode = selectNode.getNextSibling()) != null){
				projection = parseProjection(selectNode);
				projections.add(projection);
			}
			
			queryInfo.setProjections(projections);
			if(logger.isDebugEnabled()){
				logger.debug("Projections: " + projections);
			}
			
			
			
		}else{
			throw new IllegalArgumentException("Must be select ");
		}
		
	}
	
	
	
	protected String parseProjection(AST node){
		if(isType(node, HqlTokenTypes.DOT)){
			final int nodeCount = node.getNumberOfChildren();
			final StringBuilder projection = new StringBuilder();
			for(int i = 0; i < nodeCount; i++){
				if(i == 0){
					node = node.getFirstChild();
				}else{
					node = node.getNextSibling();
				}
				
				projection.append(node.getText()).append(DOT);
				
			}
			removeLastDot(projection);
			return projection.toString();
			
		}else{
			throw new IllegalArgumentException("Must be projection");
		}
		
	}
	
	
	
	
	protected void parseFrom(AST from, QueryInfo queryInfo){
		final String entityName;
		final String entityAlias;
		
		final Map<String, String> joinMap = Maps.newHashMap();
		
		if(isType(from, HqlTokenTypes.FROM)){
			final AST range = from.getFirstChild();
			if(isType(range, HqlTokenTypes.RANGE)){
				final AST entityAst = range.getFirstChild();
				entityName = entityAst.getText();
				queryInfo.setEntityName(entityName);
				
				final AST entityAliasAst = entityAst.getNextSibling();
				entityAlias = entityAliasAst.getText();
				joinMap.put(entityAlias, entityName);
				queryInfo.setEntityAlias(entityAlias);
				if(logger.isDebugEnabled()){
					logger.debug("entityName= " + entityName + ", entityAlias=" + entityAlias);
				}
				
				
				
				final int fromChildrenCount = from.getNumberOfChildren();
				if(fromChildrenCount > 1){
					AST currentNode = range;
					AST rangeNextSibling;
					AST leftJoinNode;
					int joinChildCount;
					AST joinChild = null;
					AST dotNode;
					
					
					StringBuilder joinPath = null;
					AST joinAliasNode;
					String joinAlias;
					for(int i = 1; i < fromChildrenCount; i++){
						rangeNextSibling = currentNode.getNextSibling();
						if(isType(rangeNextSibling, HqlTokenTypes.JOIN)){
							leftJoinNode  = rangeNextSibling.getFirstChild();
							if(isType(leftJoinNode, HqlTokenTypes.LEFT)){
								dotNode = leftJoinNode.getNextSibling();
								if(isType(dotNode, HqlTokenTypes.DOT)){
									
								
									joinChildCount = dotNode.getNumberOfChildren();
									if (joinChildCount > 0) {
										joinPath = new StringBuilder();
										for (int j = 0; j < joinChildCount; j++) {
											if (j == 0) {
												joinChild = dotNode.getFirstChild();
											} else {
												joinChild = joinChild.getNextSibling();
											}
											joinPath.append(joinChild.getText()).append(DOT);
											
										}
										removeLastDot(joinPath);
									}
									joinAliasNode = dotNode.getNextSibling();
									joinAlias = joinAliasNode.getText();
									if (logger.isDebugEnabled()) {
										logger.debug("Join path: " + joinPath + ", join alias: " + joinAlias);
									}
									joinMap.put(joinAlias, joinPath.toString());
									
								}
								
							}
							
						}
						currentNode = rangeNextSibling;
					}
				}
			}
			queryInfo.setJoinAliasMap(joinMap);
		}
	}
	
	
	
	protected boolean isType(AST ast, int type){
		return ast.getType() == type;
	}
	
	
	
	protected void removeLastDot(StringBuilder sb){
		if(sb != null){
			final int length = sb.length();
			if(length > 0){
				final int lastCharIndex = length-1;
				final char lastChar = sb.charAt(lastCharIndex);
				if(DOT_CHAR == lastChar){
					sb.deleteCharAt(lastCharIndex);
				}
			}
		}
	}
	
	
	
	protected  class QueryInfo implements Serializable{
		
		private String entityName;
		private String entityAlias;
		private List<String> projections;
		
		private Map<String, String> joinAliasMap;
		private List<String> properties;

		public String getEntityName() {
			return entityName;
		}

		public void setEntityName(String entityName) {
			this.entityName = entityName;
		}

		public String getEntityAlias() {
			return entityAlias;
		}

		public void setEntityAlias(String entityAlias) {
			this.entityAlias = entityAlias;
		}

		public List<String> getProjections() {
			return projections;
		}

		public void setProjections(List<String> projections) {
			this.projections = projections;
		}

		public Map<String, String> getJoinAliasMap() {
			return joinAliasMap;
		}

		public void setJoinAliasMap(Map<String, String> joinAliasMap) {
			this.joinAliasMap = joinAliasMap;
		}
		
		

		public List<String> getProperties() {
			return properties;
		}

		public void setProperties(List<String> properties) {
			this.properties = properties;
		}

		@Override
		public String toString() {
			return "QueryInfo [entityName=" + entityName + ", entityAlias=" + entityAlias + ", projections=" + projections + ", joinAliasMap=" + joinAliasMap + ", properties=" + properties + "]";
		}

		
		
		
		
		
		
		
	}
	
	
	
	protected class EntityKey implements Serializable {
		private String property;
		private Object value;
		
		
		
		
		private EntityKey(String property, Object value) {
			super();
			this.property = property;
			this.value = value;
		}
		public String getProperty() {
			return property;
		}
		public void setProperty(String property) {
			this.property = property;
		}
		public Object getValue() {
			return value;
		}
		public void setValue(Object value) {
			this.value = value;
		}
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result + ((property == null) ? 0 : property.hashCode());
			result = prime * result + ((value == null) ? 0 : value.hashCode());
			return result;
		}
		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			EntityKey other = (EntityKey) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (property == null) {
				if (other.property != null)
					return false;
			} else if (!property.equals(other.property))
				return false;
			if (value == null) {
				if (other.value != null)
					return false;
			} else if (!value.equals(other.value))
				return false;
			return true;
		}
		private ArrayToEntityResultTransformer getOuterType() {
			return ArrayToEntityResultTransformer.this;
		}
		@Override
		public String toString() {
			return "EntityKey [property=" + property + ", value=" + value + "]";
		}
		
		
		
	}
	
	
	

}
